﻿using System;
using System.Collections.Generic;
using Test30.Core;
using Test30.Util;

namespace Test30.Engine
{
    public abstract class Entity : Thing, ICollectible<EntityCollection, Entity>, ISpeed
    {
        public const int DodgeBase = 20;
        public static float GetArmorReduction(int armor)
        {
            // damage is never increased
            armor = Math.Max(0, armor);

            return 1.0f / (1.0f + armor / 50.0f);
        }

        public readonly GameEvent<Entity, EventArgs> Changed = new GameEvent<Entity, EventArgs>();
        public Speed Speed { get; }
        public Energy Energy { get; }
        public FluidStat Health { get; }
        public EntityConditions Conditions { get; }
        public bool IsAlive => Health.Current > 0;
        public Behavior Behavior => _behavior;
        public override Dungeon Dungeon => _collection.Dungeon;

        protected Entity(Vec pos,int speed,int health) : base(pos)
        {
            Energy = new Energy(this);

            Speed = new Speed(speed);
            Speed.Changed += Speed_Changed;

            Conditions = new EntityConditions(this);
            Conditions.ConditionChanged += Conditions_ConditionChanged;

            Health = new FluidStat(health);
            Health.Changed += Health_Changed;
            Health.BonusChanged += Health_BonusChanged;
        }
        public virtual void AfterDamage(NotNull<Action> action, NotNull<Hit> hit)
        {
            // apply the elemental effect if not resisted
            if (OnGetResistance(hit.Value.Attack.Element) >= 1.0f)
            {
                switch (hit.Value.Attack.Element)
                {
                    case Element.Air: action.Value.AddAction(new TeleportAction(this, hit, 3)); break;
                    case Element.Earth:
                        // cuts?
                        break;
                    case Element.Fire:
                        // burn up items
                        break;
                    case Element.Water: action.Value.AddAction(new PushBackAction(this, hit)); break;
                    case Element.Metal: /* no side effect */ break;
                    case Element.Wood: /* no side effect */ break;
                    case Element.Acid:
                        // damage equipment
                        break;
                    case Element.Cold: action.Value.AddAction(new FreezeAction(this, hit.Value.Damage)); break;
                    case Element.Lightning:
                        // break glass
                        break;
                    case Element.Poison: action.Value.AddAction(new PoisonAction(this, hit.Value.Damage)); break;
                    case Element.Dark:
                        // blind
                        break;
                    case Element.Light:
                        // blind
                        break;
                    case Element.Anima: /* no side effect */ break;
                    case Element.Death: action.Value.AddAction(new DiseaseAction(this, hit.Value.Damage)); break;
                }
            }

            // other side effects
            if (hit.Value.Attack.Flags.Has("slow"))
            {
                action.Value.AddAction(new SlowAction(this, hit.Value.Damage));
            }
            else if (hit.Value.Attack.Flags.Has("disease"))
            {
                action.Value.AddAction(new DiseaseAction(this, hit.Value.Damage));
            }

        }

        public void Die(Action action)
        {
            if (OnDie(action))
            {
                _collection.Remove(this);
            }
        }
        public abstract Attack GetAttack(Entity defender);
        public virtual int GetDodge()
        {
            //### bob: monster should override
            return DodgeBase;
        }

        public bool StandsFirm(Hit hit)
        {
            // if the element of the attack is resisted, always stand firm
            if (OnGetResistance(hit.Attack.Element) < 1.0f) return true;

            // give the derived one a chance to resist too
            if (OnStandsFirm(hit)) return true;

            // by default, the odds of resisting are half the fraction of the max health that
            // the damage is doing. so, if the damage is taking half the entity's health,
            // the odds of resisting are 1 in 4.
            return Rng.Int(Health.Max) < (hit.Damage / 2);
        }
        public override void Hit(Action action, Hit hit)
        {
            bool madeContact = true;

            // give the entity a chance to dodge
            if (hit.CanDodge)
            {
                // ask the defender how hard it is to hit
                int strike = GetDodge();

                // modify it by how good this entity is at hitting
                // subtract so that a positive bonus is good for the attacker
                strike -= hit.Attack.StrikeBonus;

                // keep it in bounds
                strike = strike.Clamp(5, 95);

                int strikeRoll = Rng.IntInclusive(1, 100);

                madeContact = (strikeRoll >= strike);
            }

            // see if we hit
            if (madeContact)
            {
                // damage the defender
                hit.SetDamage(ReceiveDamage(hit.Attack));

                // if damage was actually done
                if (hit.Damage > 0)
                {
                    Hero actingHero = Dungeon.Game.ActingEntity as Hero;

                    if (Health.Current <= 0)
                    {

                        // if a monster was killed by the hero, tell the hero so he can gain experience
                        if ((this is Monster killed))
                        {
                            actingHero?.Killed(action, killed);
                        }

                        Die(action);
                    }
                    else
                    {
                        AfterDamage(action, hit);
                    }
                }
                else
                {
                    // tell why no damage was done
                    OnGetResistance(hit.Attack.Element);
                    OnGetSlays(hit.Attack.Flags);
                }
            }
        }
        public virtual int ReceiveDamage(Attack attack)
        {
            // get the amount
            float amount = attack.Roll();

            // apply the modifiers
            amount *= OnGetResistance(attack.Element);
            amount *= OnGetSlays(attack.Flags);
            amount *= GetArmorReduction(OnGetArmor());

            // round up so that 1 damage doesn't always get cancelled out by any armor
            int appliedDamage = (int)Math.Ceiling(amount);

            // apply the damage
            Health.Current -= appliedDamage;

            //### bob: put disturb in Health_Changed? if so, need to handle *not* disturbing on auto-heal
            Behavior.Disturb();

            return appliedDamage;
        }
        public IEnumerable<Action> TakeTurn()
        {
            // get the next action from the ai
            Action turnAction = Behavior.NextAction();
            turnAction.MarkAsEnergyTaking();

            OnTakeTurn();

            yield return turnAction;

            // process the timed conditions
            foreach (Action conditionAction in Conditions.Update())
            {
                yield return conditionAction;
            }
        }

        protected virtual int OnGetArmor()
        {
            // default to no armor
            return 0;
        }
        protected abstract float OnGetResistance(Element element);
        protected abstract bool OnDie(Action action);
        protected virtual float OnGetSlays(IFlagCollection flags)
        {
            // no slays by default
            return 1.0f;
        }

        protected virtual bool OnStandsFirm(Hit hit)
        {
            // default to failing
            return false;
        }

        protected virtual void OnTakeTurn() { }

        protected void SetBehavior(NotNull<Behavior> behavior)
        {
            _behavior = behavior;
        }

        void ICollectible<EntityCollection, Entity>.SetCollection(EntityCollection collection)
        {
            _collection = collection;
        }

        #region Event handlers

        protected void OnChanged()
        {
            Changed.Raise(this, EventArgs.Empty);
        }

        private void Speed_Changed(object sender, EventArgs e)
        {
            Behavior.Disturb();
            OnChanged();
        }
        private void Conditions_ConditionChanged(object sender, EventArgs e)
        {
            Behavior.Disturb();
            OnChanged();
        }
        private void Health_Changed(object sender, EventArgs e)
        {
            OnChanged();
        }
        private void Health_BonusChanged(object sender, EventArgs e)
        {
            Behavior.Disturb();

            OnChanged();
        }
        #endregion

        private EntityCollection _collection;
        int ISpeed.Speed => Speed.Current;
        private Behavior _behavior;
    }
}
